﻿using System;
using System.Linq;
using System.Linq.Expressions;

namespace SBToolkit.Expressions
{
    /// <summary>
    /// Class provides help methods to work with expressions.
    /// </summary>
    public static class Predicate
    {
        /// <summary>
        /// Expression that takes object of type <typeparamref name="T"/> and returns value true.
        /// </summary>
        /// <typeparam name="T">Type of object to operate inside the predicate.</typeparam>
        /// <returns>Resulting expression.</returns>
        public static Expression<Func<T, bool>> True<T>()
        {
            return f => true;
        }

        /// <summary>
        /// Expression that takes object of type <typeparamref name="T"/> and returns value false.
        /// </summary>
        /// <typeparam name="T">Type of object to operate inside the predicate.</typeparam>
        /// <returns>Resulting expression.</returns>
        public static Expression<Func<T, bool>> False<T>()
        {
            return f => false;
        }

        /// <summary>
        /// Expression that takes two expressions of type <typeparamref name="T"/> and applies specified binary operation.
        /// </summary>
        /// <typeparam name="T">Type of object to operate inside the predicate.</typeparam>
        /// <param name="leftExpr">The left expression to operate on.</param>
        /// <param name="binaryType">Binary expression to apply.</param>
        /// <param name="rightExpr">The right expression to operate on.</param>
        /// <returns>Resulting expression.</returns>
        public static Expression<Func<T, bool>> BinaryExpressionBuilder<T>(this Expression<Func<T, bool>> leftExpr, ExpressionType binaryType, Expression rightExpr)
        {
            // Invoke that lambda with parameter and give the bool back
            var rightInvoke = Expression.Invoke(rightExpr, leftExpr.Parameters.Cast<Expression>());

            // make a binary expression between the results (i.e. AndAlso(&&), OrElse(||), etc)
            var binExpression = Expression.MakeBinary(binaryType, leftExpr.Body, rightInvoke);

            // Wrap it in a lambda and send it back
            return Expression.Lambda<Func<T, bool>>(binExpression, leftExpr.Parameters);
        }

        /// <summary>
        /// Expression that takes two expressions of type <typeparamref name="T"/> and ORs them.
        /// </summary>
        /// <typeparam name="T">Type of object to operate inside the predicate.</typeparam>
        /// <param name="leftExpr">The left expression to operate on.</param>
        /// <param name="rightExpr">The right expression to operate on.</param>
        /// <returns>Resulting expression.</returns>
        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> leftExpr, Expression<Func<T, bool>> rightExpr)
        {
            return leftExpr.BinaryExpressionBuilder(ExpressionType.AndAlso, rightExpr);
        }

        /// <summary>
        /// Expression that takes two expressions of type <typeparamref name="T"/> and ANDs them.
        /// </summary>
        /// <typeparam name="T">Type of object to operate inside the predicate.</typeparam>
        /// <param name="leftExpr">The left expression to operate on.</param>
        /// <param name="rightExpr">The right expression to operate on.</param>
        /// <returns>Resulting expression.</returns>
        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> leftExpr, Expression<Func<T, bool>> rightExpr)
        {
            return leftExpr.BinaryExpressionBuilder(ExpressionType.OrElse, rightExpr);
        }

        /// <summary>
        /// Expression that takes object of type <typeparamref name="T"/> and returns equality expression.
        /// </summary>
        /// <typeparam name="T">Type of object to operate inside the predicate.</typeparam>
        /// <param name="propertyName">Property name of the object (left part of equality).</param>
        /// <param name="value">Value (right part of the equality).</param>
        /// <returns>Resulting expression.</returns>
        public static Expression<Func<T, bool>> Equal<T>(string propertyName, object value)
        {
            if (typeof(T).GetProperty(propertyName) == null)
            {
                throw new ArgumentException("Object doesn't contain specified property.");
            }

            var t = Expression.Parameter(typeof(T), "t");
            var property = Expression.Property(t, propertyName);
            var comparison = Expression.Equal(property, Expression.Constant(value));
            var expression = Expression<Func<T, bool>>.Lambda(comparison, t);

            return (Expression<Func<T, bool>>)expression;
        }
    }
}